<?php
/**
 * dbfront
 * (c) 2010-2013 Stephen Adkins <spadkins@gmail.com>
 * This software began development in 2010.
 * It is based on code dating from 1999.
 *
 * License: GPL 2.0
 * The contents of this file may be used under the terms of
 * the GNU General Public License Version 2 or later (the "GPL").
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 * */
namespace API;
class File extends \App\SessionObject {
    /**
     * save(...) - save data in a file
     * @param string  overwrite_mode [IN] - (optional) (default="error") overwrite=overwrite existing file, error=do nothing but error if file already exists
     * @param string  directory [IN] - the name of the directory the file should be put into.
     * @param string  filename [IN] - the name of the file to save within the directory
     * @param string  filetype [IN] - (optional) (default="text") text=apply CR-LF=>LF conversion, binary=no conversion
     * @param string  data [IN] - data to be saved in the file
     * @param boolean success   [OUT] - indicates whether the call was successful
     */
    public function save ($request, &$response) {
        global $context, $options, $debug_sql, $trace;
        if ($trace) trace_entry();

        $test_mode = isset($options['test_mode']) ? $options['test_mode'] : 0;
        $user_id = $context->getAuthenticatedUserId();
        $user_document_root = $context->getOwnerDir();

        if (!isset($request['directory'])) throw new \Exception("ERROR: directory parameter must be provided.");
        $directory = $request['directory'];
        if (preg_match('/\\.\\./', $directory)) throw new \Exception("ERROR: directory parameter may not include '..'");
        $path = "$user_document_root/$directory";
        if (!is_dir($path)) {
            throw new \Exception("ERROR: $path does not exist.");
        }

        if (!isset($request['filename'])) throw new \Exception("ERROR: filename parameter must be provided.");
        $filename = $request['filename'];
        if (preg_match('!/!', $filename)) throw new \Exception("ERROR: filename parameter may not include '/'");

        if (!isset($request['data'])) throw new \Exception("ERROR: data parameter must be provided.");
        $data = $request['data'];
        $bytes_received = strlen($data);

        if ($user_id == APP_GUEST_USER_ID && !$test_mode) {
            throw new \Exception("ERROR: You must log in to save files.");
        }
  
        $overwrite_mode = isset($request['overwrite_mode']) ? $request['overwrite_mode'] : 'error';
        if ($overwrite_mode !== 'overwrite') {
            if (is_file("$path/$filename")) {
                throw new \Exception("ERROR: File '$filename' already exists in the directory '$directory'.");
            }
        }

        $filetype = isset($request['filetype']) ? $request['filetype'] : 'text';
        if ($filetype === 'text') {
            $data = str_replace("\r\n","\n", $data);
        }

        $fh = @fopen("$path/$filename","w");
        if ($fh === false) {
            $message = "Unable to open file for saving";
            $error = error_get_last();
            if (isset($error) && isset($error['message'])) {
                $errmsg = $error['message'];
                $errmsg  = preg_replace('/ *\\([^\\(\\)]*\\)/', '', $errmsg);
                if (preg_match('/^.*: *(.*)$/', $errmsg, $matches)) {
                    $errmsg = $matches[1];
                }
                $message .= ": $errmsg";
            }
            throw new \Exception($message);
        }
        $bytes_written = @fwrite($fh, $data);
        if ($bytes_written === false) {
            $message = "Error writing data";
            $error = error_get_last();
            if (isset($error) && isset($error['message'])) {
                $errmsg = $error['message'];
                $errmsg  = preg_replace('/ *\\([^\\(\\)]*\\)/', '', $errmsg);
                if (preg_match('/^.*: *(.*)$/', $errmsg, $matches)) {
                    $errmsg = $matches[1];
                }
                $message .= ": $errmsg";
            }
            throw new \Exception($message);
        }
        fclose($fh);
        
        $response['success'] = true;
        $response['bytes_received'] = $bytes_received;
        $response['bytes_written'] = $bytes_written;
        if ($trace) trace_exit($response);
    }

    /**
     * upload(...) - serves as the URL endpoint for a file upload
     * $_FILES (superglobal) assoc-array $_FILES [IN] - the uploaded file, its filename, etc. are via PHP's $_FILES variable
     * @param string  overwrite_mode [IN] - (optional) overwrite=overwrite existing file, increment=create new file with incremented extension if exists, error=do nothing but error if file already exists
     * @param string  formfieldname [IN] - (optional) the name of the file upload form field (e.g. 'uploadfile' in <input type="file" name="uploadfile">)
     * @param string  directory [IN] - (optional, default "upload") the name of the directory the file should be put into.
     * @param boolean success   [OUT] - indicates whether the call was successful
     */
    public function upload ($request, &$response) {
        global $context, $options, $debug_sql, $trace;
        if ($trace) trace_entry();

        $test_mode = isset($options['test_mode']) ? $options['test_mode'] : 0;
        $user_id = $context->getAuthenticatedUserId();
        $user_document_root = $context->getOwnerDir();
        if (isset($request['directory'])) {
            $path = "$user_document_root/$request[directory]";
            $response['directory'] = $request['directory'];
        }
        else {
            $path = "$user_document_root/upload";
            $response['directory'] = 'upload';
        }

        if (!is_dir($path)) {
            throw new \Exception("ERROR: $path does not exist.");
        }

        $formfieldname_reqd = isset($request['formfieldname']) ? $request['formfieldname'] : null;

        #overwrite_mode: 1 = overwrite existing file, 2 = create new file with incremented extension, 3 = do nothing but error if file already exists
        $overwrite_mode = isset($request['overwrite_mode']) ? $request['overwrite_mode'] : 'increment';
        if     ($overwrite_mode === 'overwrite') { $overwrite_mode = 1; }
        elseif ($overwrite_mode === 'increment') { $overwrite_mode = 2; }
        elseif ($overwrite_mode === 'error')     { $overwrite_mode = 3; }
        else                                     { $overwrite_mode = 2; }   # default is 'increment'

        # $acceptable_types = "image/gif";  #only gif images
        # $acceptable_types = "image/gif|image/png|image/jpg"; #gif, jpg or img
        # $acceptable_types = "";  #all types accepted.</code>
        $acceptable_types = '';

        # default filename extension
        $extension = '';

        $upload = new \Upload();
        
        if ($user_id == APP_GUEST_USER_ID && !$test_mode) {
            throw new \Exception("ERROR: You must log in to upload files.");
        }
        elseif ( $_SERVER['REQUEST_METHOD'] == 'POST' && empty($_POST) &&
            empty($_FILES) && $_SERVER['CONTENT_LENGTH'] > 0 ) {
        
            $postMaxSize = ini_get('post_max_size');
        
            switch ( substr($postMaxSize,-1) ) {
                case 'G':
                    $postMaxSize = $postMaxSize * 1024;
                case 'M':
                    $postMaxSize = $postMaxSize * 1024;
                case 'K':
                    $postMaxSize = $postMaxSize * 1024;
            }

            $uploadMaxFileSize = ini_get('upload_max_filesize');
        
            switch ( substr($uploadMaxFileSize,-1) ) {
                case 'G':
                    $uploadMaxFileSize = $uploadMaxFileSize * 1024;
                case 'M':
                    $uploadMaxFileSize = $uploadMaxFileSize * 1024;
                case 'K':
                     $uploadMaxFileSize = $uploadMaxFileSize * 1024;
            }

            if ($postMaxSize < $uploadMaxFileSize) {
                $uploadMaxFileSize = $postMaxSize;
            }    

            $error = 'Uploaded file is too large. '.
                $_SERVER['CONTENT_LENGTH'].
                ' bytes exceeds the maximum size of '.
                $uploadMaxFileSize.' bytes.';

            throw new \Exception("ERROR: $error");
        }
        elseif (count($_FILES) == 0) {
            throw new \Exception("ERROR: No file uploaded.");
        }
        else {
            foreach ($_FILES as $formfieldname => $fileinfo) {
                if (!$formfieldname_reqd || $formfieldname === $formfieldname_reqd) {
                    if ($upload->verify($formfieldname, $acceptable_types, $extension)) {
                        $filename = $upload->save_file($path, $overwrite_mode);
                        if (!$filename) {
                            $errormsg = implode(' ', array_values($upload->errors));
                            throw new \Exception("ERROR: ".$errormsg);
                        }
                        $response['filename'] = $filename;
                    }
                    else {
                        $errormsg = implode(' ', array_values($upload->errors));
                        throw new \Exception("ERROR: ".$errormsg);
                    }
                }
            }
        }
        
        $response['success'] = true;
        if ($trace) trace_exit($response);
    }

    /**
     * import(...) - serves as the URL endpoint for a file upload
     * $_FILES (superglobal) assoc-array [IN] - the uploaded file, its filename, etc. are via PHP's $_FILES variable
     * @param string  overwrite_mode     [IN] - (optional) overwrite=overwrite existing file, increment(default)=create new file with incremented extension if exists, error=do nothing but error if file already exists
     * @param string  create_mode        [IN] - (optional) create=create a table if it doesn't exist, none(default)=the table must already exist
     * @param string  import_mode        [IN] - (optional) insert=insert data with error on collision, update=insert rows but update them instead if they already exist, replace=delete all existing rows and insert these instead
     * @param string  formfieldname      [IN] - (optional) the name of the file upload form field (e.g. 'uploadfile' in <input type="file" name="uploadfile">)
     * @param boolean success            [OUT] - indicates whether the call was successful
     */
    public function import ($request, &$response) {
        global $context, $options, $debug_sql, $trace;
        if ($trace) trace_entry();

        $test_mode  = isset($options['test_mode'])  ? $options['test_mode']  : 0;
        $filename   = isset($request['filename'])   ? $request['filename']   : null;
        $db_name    = isset($request['db_name'])    ? $request['db_name']    : null;
        $table_name = isset($request['table_name']) ? $request['table_name'] : null;

        $import_options = array();
        if (isset($request['create_mode'])) $import_options['create_mode'] = $request['create_mode'];
        if (isset($request['import_mode'])) $import_options['import_mode'] = $request['import_mode'];

        $user_id = $context->getAuthenticatedUserId();
        $user_document_root = $context->getOwnerDir();
        $user_reldir = "upload";
        $path = "$user_document_root/$user_reldir";

        if (!$db_name && !$table_name && preg_match('/^([a-z0-9_]+)\\.([a-z0-9_]+)\\./', $filename, $matches)) {
            $db_name    = $matches[1];
            $table_name = $matches[2];
        }
        if (!$db_name) throw new \Exception("ERROR: db_name is not suppied, either explicitly or implicitly by naming the file '{db_name}.{table_name}.*.csv'");
        if (!$table_name) throw new \Exception("ERROR: table_name is not suppied, either explicitly or implicitly by naming the file '{db_name}.{table_name}.*.csv'");

        if ($filename) {
            if (!file_exists("$path/$filename")) {
                throw new \Exception("ERROR: File \"$filename\" does not exist.");
            }
        }
        else {
            $this->upload($request, $response);
            $filename = isset($response['filename']) ? $response['filename'] : null;
            $response['directory'] = $user_reldir;
        }
        if (!$filename) throw new \Exception("ERROR: filename could not be determined");

        $db = $context->database($db_name);
        $db->import_table($table_name, "$path/$filename", $import_options);
        
        $response['success'] = true;
        if ($trace) trace_exit($response);
    }

    /**
     * export(...) - serves as the URL endpoint for a file upload
     * $_FILES (superglobal) assoc-array [IN] - the uploaded file, its filename, etc. are via PHP's $_FILES variable
     * @param string  overwrite_mode     [IN] - (optional) overwrite=overwrite existing file, increment(default)=create new file with incremented extension if exists, error=do nothing but error if file already exists
     * @param string  create_mode        [IN] - (optional) create=create a table if it doesn't exist, none(default)=the table must already exist
     * @param string  export_mode        [IN] - (optional) insert=insert data with error on collision, update=insert rows but update them instead if they already exist, replace=delete all existing rows and insert these instead
     * @param string  formfieldname      [IN] - (optional) the name of the file upload form field (e.g. 'uploadfile' in <input type="file" name="uploadfile">)
     * @param boolean success            [OUT] - indicates whether the call was successful
     */
    public function export ($request, &$response) {
        global $context, $options, $debug_sql, $trace;
        if ($trace) trace_entry();

        $test_mode  = isset($options['test_mode'])  ? $options['test_mode']  : 0;
        $filename   = isset($request['filename'])   ? $request['filename']   : null;
        $db_name    = isset($request['db_name'])    ? $request['db_name']    : null;
        $table_name = isset($request['table_name']) ? $request['table_name'] : null;
        $time       = time();

        $export_options = array();
        #if (isset($request['create_mode'])) $export_options['create_mode'] = $request['create_mode'];
        #if (isset($request['export_mode'])) $export_options['export_mode'] = $request['export_mode'];

        $user_id = $context->getAuthenticatedUserId();
        $user_document_root = $context->getOwnerDir();
        $user_reldir = "export";
        $path = "$user_document_root/$user_reldir";

        if (!$db_name && !$table_name && $filename && preg_match('/^([a-z0-9_]+)\\.([a-z0-9_]+)\\./', $filename, $matches)) {
            $db_name    = $matches[1];
            $table_name = $matches[2];
        }
        if (!$db_name) throw new \Exception("ERROR: db_name is not suppied explicitly (or implicitly by naming the file '{db_name}.{table_name}.*.csv')");
        if (!$table_name) throw new \Exception("ERROR: table_name is not suppied explicitly (or implicitly by naming the file '{db_name}.{table_name}.*.csv')");
        if (!$filename) {
            $timestamp = date('Ymd-His', $time);
            $filename = "$db_name.$table_name.$timestamp.csv";
        }

        $response['filename']  = $filename;
        $response['directory'] = $user_reldir;

        if ($filename) {
            if (file_exists("$path/$filename")) {
                throw new \Exception("ERROR: File \"$filename\" already exists.");
            }
        }

        $db = $context->database($db_name);
        $db->export_table($table_name, "$path/$filename", $export_options);
        
        $response['success'] = true;
        if ($trace) trace_exit($response);
    }

    /**
     * filetree(...) - serves as the URL endpoint for an ExtJS tree whose nodes represent the file/directory system of the current group
     * In this routine, all tree node ID's are represented by file/directory paths from the root of the group's directory tree.
     * (op = 'mkdir')   requires 'dir_path'. optionally 'dir_name'. response: {'success': true} - creates the new 'dir_name' directory in the existing 'dir_path' directory
     * (op = 'delete')  requires 'dir_path'.                        response: {'success': true} - removes a file or directory (recursively if 'force' option is given)
     * (op = 'move')    requires 'from', 'to'.                      response: {'success': true} - moves source file or directory to the target file or directory
     * (op = 'getTree')
     * (1) if 'node' is 'src', the current directory is the user's home directory, and the list of all files and directories in it is returned.
     * (1)               {'id'=>{id}, 'text'=>{filename}, 'expanded'=>false, 'leaf'=>{leafInd}}
     * (2) if 'node' is not 'src', then 'node' is the relative subdirectory from the user's home directory, and the list of all files and directories in it is returned.
     * (2) The response is then a JSON array of children, describing the children of the 'node' queried.
     * (2)               {'id'=>{id}, 'text'=>{filename}, 'expanded'=>false, 'leaf'=>{leafInd}}
     * (The design of this API is based solely on the current understanding of what ExtJS trees naturally expect.)
     * (It is possible that the API could be cleaned up or made more consistent by using certain undetermined configuration settings on the ExtJS tree.)
     * @param string  op        [IN] - (defaults to 'getTree') the operation ('getTree', 'move', 'delete', or 'mkdir')
     * @param string  node_type [IN] - [used for 'getTree'] - only directory contents which match 'directory' or 'file' types are returned
     * @param string  glob      [IN] - [used for 'getTree'] - only directory contents which match the glob pattern (e.g. *.csv) are returned
     * @param string  from      [IN] - [used for 'move'] - relative source file name or directory (relative to the group's root directory) (without '..')
     * @param string  to        [IN] - [used for 'move'] - relative source file name or directory (relative to the group's root directory) (without '..')
     * @param string  dir_path  [IN] - [used for 'mkdir', 'delete'] - a relative path to a directory (or file) (relative to the group's root directory) (without '..')
     * @param string  dir_name  [IN] - [used for 'mkdir'] (optional) - a new directory to create in the dir_path (without any '/')
     * @param string  force     [IN] - [used for 'delete'] if the 'dir_path' is a non-empty directory, 'force' will force it to be deleted
     * @param boolean success   [OUT] - indicates whether the call was successful
     * @param string  message   [OUT] - if (success == false), the message will tell why it failed
     * @param string  id        [OUT] - 'src' at the root directory
     * @param string  text      [OUT] - 'files' is the label of the root node
     * @param array   children  [OUT] - nested array of child objects
     */
    public function filetree ($request, &$response) {
        global $context, $options, $debug_sql, $trace;
        if ($trace) trace_entry();

        $test_mode = isset($options['test_mode']) ? $options['test_mode'] : 0;
        $op        = isset($request['op'])        ? $request['op']        : 'getTree';
        $neat      = (isset($request['neat']) && $request['neat']) ? 1    : 0;
        $user_document_root = $context->getOwnerDir();
        
        if ($op === 'getTree') {
            $node = isset($request['node']) ? $request['node'] : 'src';
            $glob = isset($request['glob']) ? $request['glob'] : '*';
            $reqd_node_type = isset($request['node_type']) ? $request['node_type'] : null;
            if ($node === 'src') {
                $dir = $user_document_root;
                $item_prefix = '';
            }
            elseif (preg_match('!^[a-zA-Z0-9/\\._-]+$!', $node) && !preg_match('/\\.\\./', $node)) {
                $dir = "$user_document_root/$node";
                $item_prefix = "$node/";
            }
            else {
                throw new \Exception("ERROR: illegal node in tree [$node]");
            }
            foreach (glob("$dir/$glob") as $filepath) {
                $filename = basename($filepath);
                $id       = "{$item_prefix}{$filename}";
                if (is_dir($filepath)) {
                    if ($reqd_node_type === null || $reqd_node_type === 'directory') {
                        $response[] = array('id'=>$id, 'text'=>$filename, 'node_type'=>'directory', 'expanded'=>false);
                    }
                }
                elseif (is_file($filepath)) {
                    if ($reqd_node_type === null || $reqd_node_type === 'file') {
                        $response[] = array('id'=>$id, 'text'=>$filename, 'node_type'=>'file', 'leaf'=>true);
                    }
                }
            }
        }
        else if ($op === 'move') {
            if (! isset($request['from']) || strlen($request['from']) === 0) throw new \Exception("ERROR: Source path must be supplied");
            if (! isset($request['to'])   || strlen($request['to'])   === 0) throw new \Exception("ERROR: Destination path must be supplied");
            if (preg_match("/\.\./",$request['to']))   throw new \Exception("Invalid path name ('..' included)");
            if (preg_match("/\.\./",$request['from'])) throw new \Exception("Invalid path name ('..' included)");
            
            $from = preg_replace("/\/\/+/", "/", $user_document_root . "/" . $request['from']);   # remove duplicate '/'s
            if (! file_exists($from)) throw new \Exception("ERROR: Source file does not exist");

            $to   = preg_replace("/\/\/+/", "/", $user_document_root . "/" . $request['to']);     # remove duplicate '/'s
            if (is_dir($from) && is_dir($to)) $to = $to . '/' . basename($from);
            if (file_exists($to)) throw new \Exception("ERROR: Destination file already exists");
            
            if (rename($from,$to) === false) throw new \Exception("Unable to move file or directory");
        }
        else if ($op === 'mkdir') {
            $dir_path = (isset($request['dir_path']) && $request['dir_path']) ? $request['dir_path'] : '';
            if (!$dir_path) throw new \Exception("ERROR: New directory path (dir_path) must be supplied");
            if (preg_match("/\.\./",$dir_path)) throw new \Exception("Invalid directory path (includes '..')");
            $new_dir = $user_document_root . "/" . $dir_path;
            $dir_name = (isset($request['dir_name']) && $request['dir_name']) ? $request['dir_name'] : '';
            if ($dir_name) {
                if (preg_match("/\.\./",$dir_name)) throw new \Exception("Invalid directory name (includes '..')");
                $new_dir .= '/'.$dir_name;
            }
            if (mkdir($new_dir, 02775, true) === false) throw new \Exception("Unable to create directory");
        }
        else if ($op === 'delete') {
            if (! isset($request['dir_path']) || strlen($request['dir_path'] === 0)) throw new \Exception("ERROR: Directory path must be supplied");
            
            $dir_path = $request['dir_path'];
            $dir = preg_replace("/\/\//", "/", $user_document_root . '/' . $dir_path);

            if (is_dir($dir)) {
                $dir_contents = scandir($dir);
                if (! $this->_is_dir_empty($dir)) {
                    if (isset($request['force']) && $request['force'] == 1) {
                        $path = $dir;
                        while (count(scandir($path)) != 2 || ($path !== $dir)) {
                            foreach (scandir($path) as $item) {
                                if ($item == '.' || $item == '..')
                                    continue;
                                
                                $item = "{$path}/{$item}";
                                
                                if (is_file($item)) {
                                    if (unlink($item) === false)
                                        throw new \Exception("ERROR: could not remove file [{$item}]");
                                }
                                else if (count(scandir($item)) > 2) {
                                    $path = $item;
                                    break;
                                }
                                else if (rmdir($item) === false)
                                    throw new \Exception("ERROR: could not empty directory");
                                
                                // if dir is empty move up one dir
                                if (count(scandir($path)) == 2 && $path != $dir)
                                    $path = preg_replace("/\/[^\/]+[\/]{0,1}$/","",$path);
                            }
                        }
                    }
                    else {
                        $message = "Directory is not empty [$dir_path]";
                        if ($test_mode) $message .= " ($dir)";
                        throw new \Exception($message);
                    }
                }
                if (rmdir($dir) === false) {
                    $message = "Unable to remove directory [$dir_path]";
                    if ($test_mode) $message .= " ($dir)";
                    throw new \Exception($message);
                }
            }
            else if (is_file($dir)) {
                if (unlink($dir) == false) {
                    $message = "Unable to remove file [$dir_path]";
                    if ($test_mode) $message .= " ($dir)";
                    throw new \Exception($message);
                }
            }
            else {
                $message = "File or directory does not exist [$dir_path]";
                if ($test_mode) $message .= " ($dir)";
                throw new \Exception($message);
            }
        }
        else {
            $response['success'] = false;
            $response['message'] = "Cannot perform operation ('$op' is not a valid operation)";
        }
        if ($trace) trace_exit($response);
    }

    private function _is_dir_empty ($dir) {
      if (!is_readable($dir)) return null; 
      $dirh = opendir($dir);
      $empty = true;
      while (false !== ($entry = readdir($dirh))) {
          if ($entry !== '.' && $entry !== '..') {
              $empty = false;
              break;
          }
      }
      closedir($dirh);
      return true;
    }
}
